fix: include chain ID audience claim in JWT created by create-api-key#513
Conversation
The verifier (aggregator/auth.go::verifyAuth, tightened in #509) requires the JWT to have an `aud` claim containing the chain ID string. But `CreateAdminKey` was building the JWT with only ExpiresAt, Issuer, Subject, and Roles. As a result, every key generated by the `create-api-key` CLI was rejected with "API key is invalid" — breaking ava-sdk-js's test:core CI which generates a fresh key per run. Dial the eth RPC inside CreateAdminKey to fetch the chain ID (NewAggregator does not run the lifecycle init that would populate agg.chainID), then add `Audience: jwt.ClaimStrings{chainID.String()}` to the registered claims. Discovered while debugging AvaProtocol/ava-sdk-js#209 CI failures.
There was a problem hiding this comment.
Pull request overview
This PR fixes create-api-key generated JWTs being rejected by the aggregator auth verifier by adding the required aud (audience) claim containing the chain ID string.
Changes:
- Dial an Ethereum RPC inside
CreateAdminKeyto fetch the chain ID. - Populate
jwt.RegisteredClaims.Audiencewith the chain ID string when generating admin API keys.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| rpcClient, err := ethclient.Dial(nodeConfig.EthHttpRpcUrl) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err) | ||
| } | ||
| defer rpcClient.Close() | ||
| chainID, err := rpcClient.ChainID(context.Background()) | ||
| if err != nil { |
There was a problem hiding this comment.
The audience chain ID should match what the verifier expects (r.chainID), which is derived from the smart wallet RPC (see aggregator/rpc_server.go where smartWalletChainID comes from agg.config.SmartWallet.EthRpcUrl). Fetching chain ID from nodeConfig.EthHttpRpcUrl can produce a different value in cross-chain configs (e.g. EigenLayer on Ethereum but SmartWallet on Base), causing freshly generated keys to still fail auth. Prefer using nodeConfig.SmartWallet.ChainID (already derived in config.NewConfig) or dial nodeConfig.SmartWallet.EthRpcUrl instead of EthHttpRpcUrl.
| // The verifier (aggregator/auth.go::verifyAuth) requires the JWT to have an | ||
| // `aud` claim containing the chain ID string. NewAggregator does not run | ||
| // the lifecycle init that populates agg.chainID, so dial the RPC directly | ||
| // and read it here. Without this claim, every key generated by | ||
| // `create-api-key` is rejected with "API key is invalid". | ||
| rpcClient, err := ethclient.Dial(nodeConfig.EthHttpRpcUrl) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err) | ||
| } | ||
| defer rpcClient.Close() | ||
| chainID, err := rpcClient.ChainID(context.Background()) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to fetch chainId for audience claim: %w", err) | ||
| } |
There was a problem hiding this comment.
This introduces an extra RPC dial + ChainID call during create-api-key, but config.NewConfig already dials RPCs and stores the chain ID on nodeConfig.SmartWallet.ChainID. Avoiding the additional network round-trip (and dependency on EthHttpRpcUrl reachability) will make key generation faster and less failure-prone.
| return fmt.Errorf("failed to dial eth rpc to determine chainId for audience claim: %w", err) | ||
| } | ||
| defer rpcClient.Close() | ||
| chainID, err := rpcClient.ChainID(context.Background()) |
There was a problem hiding this comment.
Using context.Background() for the ChainID call can hang indefinitely if the RPC endpoint is slow/unreachable (common in CI). Consider using a context with timeout/cancellation (e.g., context.WithTimeout) for the chain ID fetch so create-api-key fails fast with a clear error.
| chainID, err := rpcClient.ChainID(context.Background()) | |
| chainIDCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| defer cancel() | |
| chainID, err := rpcClient.ChainID(chainIDCtx) |
| claims := &auth.APIClaim{ | ||
| RegisteredClaims: &jwt.RegisteredClaims{ | ||
| ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365 * 10)), | ||
| Issuer: "AvaProtocol", |
There was a problem hiding this comment.
Issuer is hard-coded as "AvaProtocol" here. Since the project already defines auth.Issuer, using the constant avoids accidental drift if the issuer changes in the future.
| Issuer: "AvaProtocol", | |
| Issuer: auth.Issuer, |
- Use nodeConfig.SmartWallet.ChainID directly instead of dialing the EigenLayer RPC. The verifier's r.chainID is sourced from the smart wallet RPC, so cross-chain configs (e.g. EigenLayer on Ethereum + SmartWallet on Base) would have failed with the previous approach. - Drops the extra RPC round-trip and the context-timeout concern entirely. - Use the auth.Issuer constant instead of the hard-coded "AvaProtocol" string.
…#513) Co-authored-by: Wei Lin <wei@avaprotocol.org>
Summary
aggregator/auth.go::verifyAuth, tightened in release: staging → main (event trigger fixes, fee classification, JWT API key, sentry logging) #509) requires the JWT to include anaudclaim containing the chain ID string.aggregator/key.go::CreateAdminKeywas still building JWTs with onlyExpiresAt,Issuer,Subject,Roles— noaud— so every key fromcreate-api-keywas rejected with "API key is invalid".CreateAdminKeyto fetch the chain ID (NewAggregatordoes not run the lifecycle init that would populateagg.chainID) and addAudience: jwt.ClaimStrings{chainID.String()}to the claims.Discovered while debugging AvaProtocol/ava-sdk-js#209 CI failures:
test:coregenerates a fresh key per run viadocker compose exec aggregator /ava create-api-key, and every authenticated test intests/core/auth.test.tswas returning16 UNAUTHENTICATED: User authentication error: API key is invalid.Test plan
go build ./aggregator/... ./cmd/...cleandocker compose exec aggregator /ava create-api-key --role=admin --subject=0x...and use the resulting key against the dev aggregatorava-sdk-jstest:coreagainst a:latestimage built from this branch🤖 Generated with Claude Code